9장. 임베딩 모델 파인튜닝
출처 — 브라이스 유·조경아·박수진·김재웅, 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(프리렉, 2025), 9장 (pp. 525~562). 원문 PDF
rag_master_final_v11_260910.pdf(2026-09-10 판)사전 학습된 임베딩 모델을 대조 학습으로 도메인에 맞게 파인튜닝하고, 정보 검색 평가 지표로 그 효과를 직접 측정한다. 이 책의 마지막 장이다.
학습 목표
이 장을 끝내면 다음을 할 수 있다.
- 대조 학습에서 포지티브 샘플과 네거티브 샘플의 역할을 구분하여 설명한다.
- 배치 내 네거티브 샘플링이 명시적 네거티브 준비 없이도 학습을 가능하게 하는 원리를 설명한다.
- 하드 네거티브와 이지 네거티브의 차이를 구분하고, 언제 하드 네거티브를 추가해야 하는지 판단한다.
- GPT 계열 모델로 합성 데이터(질문)를 생성해 임베딩 모델 학습 파이프라인을 구현한다.
- InformationRetrievalEvaluator로 임베딩 모델의 검색 성능을 평가하고 Accuracy·Precision·Recall·NDCG·MRR·MAP 여섯 지표를 해석한다.
전체 흐름도
[ 사전 학습된 임베딩 모델 ] 예: BAAI/bge-m3 (2장에서 검색에 쓴 그 모델)
│ 특정 도메인·복잡한 문서에서는 검색 성능이 떨어질 수 있다
▼
[ 1. 학습 원리 — 대조 학습 ]
│ 포지티브 샘플(앵커-관련 문서) 유사도는 ↑, 네거티브 샘플(앵커-무관 문서) 유사도는 ↓
│ ├─ 데이터셋 구성: 트리플렛(앵커,포지티브,네거티브) / 다중 네거티브 — 수동 준비, 노동 집약적
│ └─ 배치 내 네거티브 샘플링 — 포지티브만 준비해도 배치의 다른 샘플이 자동으로 네거티브가 된다
│ ▼
│ 손실 함수 MultipleNegativesRankingLoss — 포지티브 유사도는 높이고 네거티브 유사도는 낮추는 방향으로 최적화
▼
[ 2. 성능을 높이는 방법 ]
├─ 배치 크기 키우기 — 배치가 클수록 네거티브 수↑ (GPU 메모리가 상한선)
├─ 하드 네거티브 추가 — 이지 네거티브(무작위 자동)보다 어려운, 사람이 직접 고른 헷갈리는 오답
└─ 그 외 팁 — 실전 질의 반영 · 데이터 증강 · 학습률/온도(scale) 조정
▼
[ 3. 실전 파인튜닝 — 구글 코랩 T4 GPU ]
PDF → 텍스트 청크(학습·검증 코퍼스)
→ GPT 계열 모델로 질문 합성(포지티브 샘플 자동 생성)
→ BAAI/bge-m3 로드 → MultipleNegativesRankingLoss 로 학습
→ InformationRetrievalEvaluator 로 원본·파인튜닝 모델 평가
(Accuracy·Precision·Recall·NDCG·MRR·MAP)
▼
[ 결과 ] 전 지표에서 파인튜닝 모델이 원본보다 우세 → 도메인 특화 임베딩으로 RAG 검색 정확도 향상
0. 용어 사전
참고 — 위쪽 4개는 이 장을 읽기 전에 알아야 하는 선행 용어다. 낯설면 2장(검색 증강 생성 기초)과 8장(LLM 파인튜닝)을 먼저 보라.
| 한글 용어 | 원문 영문명 | 의미 |
|---|---|---|
| 임베딩 | Embedding | (선행) 텍스트의 의미를 벡터(숫자 배열)로 표현한 것. 의미가 비슷한 텍스트일수록 벡터가 가깝다. 2장 §1. 이 장 전체가 "이 벡터를 도메인에 맞게 더 정확히 만드는 법"을 다룬다 |
| 코사인 유사도 | Cosine Similarity | (선행) 두 벡터가 가리키는 방향이 얼마나 같은지 재는 지표. -1~1 사이 값이며 1에 가까울수록 유사하다. 2장 §1. 이 장의 손실 함수·평가 지표가 전부 이 위에서 동작한다 |
| 파인튜닝 | Fine-tuning | (선행) 사전 학습된 모델을 특정 도메인·작업에 맞게 추가 학습시키는 과정. 8장은 이를 LLM 본체에 적용했고, 이 장은 같은 개념을 임베딩 모델에 적용한다 — 대상이 다르다 |
| 구글 코랩·GPU 런타임 | Google Colab GPU Runtime | (선행) 브라우저에서 무료로 쓸 수 있는 GPU 계산 환경. 0장 §2에서 준비 방법을 다뤘다. 이 장의 실습은 T4 GPU 런타임을 전제로 한다 |
| 대조 학습 | Contrastive Learning | 의미가 비슷한 문장 쌍은 가깝게, 다른 문장 쌍은 멀게 임베딩을 조정하는 학습 방식. 본문 §9.1.1 |
| 포지티브 샘플 | Positive Sample | 기준 문서(앵커)와 의미적으로 관련 있는 비교 문서. 본문 §9.1.1 |
| 네거티브 샘플 | Negative Sample | 기준 문서와 의미적으로 무관하거나 관련성이 낮은 비교 문서. 본문 §9.1.1 |
| 앵커 | Anchor | 대조 학습에서 기준이 되는 문서(보통 검색어). 본문 §9.1.2 |
| 트리플렛 | Triplet | (앵커, 포지티브, 네거티브) 세 개를 한 묶음으로 구성한 학습 데이터 형태. 본문 §9.1.2 |
| 배치 | Batch | 모델이 한 번에 묶어서 학습하는 데이터 단위. 데이터 5,000개를 배치 40개씩 나누면 125회 학습한다. 본문 §9.1.3 |
| 배치 크기 | Batch Size | 한 배치에 포함되는 데이터 개수. 본문 §9.2.1 |
| 배치 내 네거티브 샘플링 | In-batch Negative Sampling | 같은 배치에 있는 다른 샘플의 포지티브 문서를 자동으로 네거티브로 재활용하는 방법. 명시적 네거티브 준비가 필요 없다. 본문 §9.1.3 |
| 이지 네거티브 | Easy Negative | 배치 내에서 자동으로 골라지는, 주제가 완전히 다른 네거티브. 모델이 구분하기 상대적으로 쉽다. 본문 §9.2.2 |
| 하드 네거티브 | Hard Negative | 사람이 의도적으로 고른, 포지티브와 주제는 비슷하나 정답은 아닌 네거티브. 모델에게 더 어려운 과제를 준다. 본문 §9.2.2 |
| 손실 함수 | Loss Function | 모델의 예측과 실제 정답 사이의 오차를 계산해 학습 방향을 정하는 기준식. 본문 §9.1.1 |
| MultipleNegativesRankingLoss | - | 포지티브 쌍의 유사도는 높이고 네거티브 쌍의 유사도는 낮추도록 설계된 손실 함수. 이 장 실습에서 사용. 본문 §9.1.4 |
| 온도(scale) | Temperature | 손실 함수에서 유사도 값의 민감도를 조절하는 파라미터. scale은 온도의 역수로 값이 클수록 손실 함수가 더 예민하게 반응한다. 본문 §9.1.5 |
| 워밍업 스텝 | Warmup Steps | 학습 초반에 학습률을 서서히 끌어올리는 구간의 길이. 본문 §9.1.5 |
| 학습률 | Learning Rate | 한 번의 학습 단계에서 모델 파라미터를 얼마나 크게 갱신할지 정하는 값. 본문 §9.1.5 |
| 합성 데이터 | Synthetic Data | 사람이 직접 만들지 않고 LLM 등으로 자동 생성한 학습 데이터. 이 장에서는 GPT 계열 모델로 문서마다 질문을 만들어 포지티브 샘플을 확보한다. 본문 §9.3.3 |
| InformationRetrievalEvaluator | - | sentence-transformers 가 제공하는 검색 성능 평가 도구. queries·corpus·relevant_docs 세 딕셔너리를 입력받는다. 본문 §9.3.5 |
| Accuracy@k | - | 상위 k개 검색 결과 안에 정답이 하나라도 포함되면 성공으로 세는 지표. 본문 §9.3.7 |
| Precision@k | - | 상위 k개 결과 중 정답이 차지하는 비율. 본문 §9.3.7 |
| Recall@k | - | 전체 정답 중 상위 k개 안에 포함된 정답의 비율. 본문 §9.3.7 |
| NDCG | Normalized Discounted Cumulative Gain | 정답이 높은 순위에 있을수록 높은 점수를 주는 지표 — 순위까지 고려한다. 본문 §9.3.7 |
| MRR | Mean Reciprocal Rank | 정답이 처음 등장한 순위의 역수를 평균한 지표. 정답을 얼마나 빨리 찾는지를 잰다. 본문 §9.3.7 |
| MAP | Mean Average Precision | 정답을 찾을 때마다의 Precision 값을 평균한 지표. 검색 결과의 전반적 정확도·일관성을 잰다. 본문 §9.3.7 |
1. 임베딩 모델 학습의 원리
임베딩 모델 파인튜닝은 사전 학습된 임베딩 모델을 특정 도메인이나 작업에 맞게 최적화하는 과정이다. 대규모 언어 모델이 정확한 답변을 내려면 효과적인 문서 검색이 필수인데, 전문 분야 문서나 복잡한 내용을 다룰 때는 일반 임베딩 모델의 검색 성능이 떨어질 수 있다. 이때 임베딩 모델을 파인튜닝하면 검색 정확도가 올라가고, 결과적으로 RAG 시스템 전체의 답변 품질이 개선된다.
임베딩 학습의 핵심 목표는 의미적으로 유사한 텍스트는 임베딩 공간에서 가깝게, 의미적으로 다른 텍스트는 멀리 위치하도록 만드는 것이다. 즉 실제로 의미가 비슷한 문장 쌍에는 높은 임베딩 유사도를, 의미가 다른 문장 쌍에는 낮은 유사도를 반환하도록 임베딩 벡터를 업데이트한다. 이를 위해 의미가 유사한 문장 쌍과 유사하지 않은 문장 쌍을 대조하여 학습하는 방식, 즉 대조 학습(contrastive learning)을 쓴다.
9.1.1 대조 학습
대조 학습을 이해하려면 학습 데이터를 만들 때 반드시 필요한 포지티브 샘플(positive sample)과 네거티브 샘플(negative sample)을 알아야 한다.
- 포지티브 샘플 — 의미적으로 관련이 있는 문장 쌍. 예: (기준 문서 "서울의 인구는?", 비교 문서 "서울의 인구는 약 970만 명입니다.")
- 네거티브 샘플 — 포지티브 샘플과 대조되는 데이터로, 기준 문서는 같지만 비교 문서가 의미적으로 관련이 없거나 관련성이 낮은 문장 쌍. 예: (기준 문서 "서울의 인구는?", 비교 문서 "파리는 프랑스의 수도입니다.")
학습 데이터를 만들 때 포지티브 샘플은 실제 RAG를 수행할 때 사용자가 입력할 만한 검색어를 기준 문서로, 검색 결과로 유사도가 높게 나오길 바라는 문서를 관련 문서로 삼아 구성한다. 반면 네거티브 샘플은 같은 앵커에 대해 검색 결과에 포함되지 않기를 바라는 문서를 짝지어 구성한다.
일반적인 임베딩 학습은 다음 세 단계를 따른다.
- 포지티브·네거티브 샘플 구성 — 기준 문서를 중심으로 유사도가 높은 포지티브 쌍과 유사도가 낮은 네거티브 쌍을 모두 학습 데이터로 준비한다.
- 대조 학습 — 모델이 포지티브 샘플 쌍의 임베딩 간 거리는 가깝게, 네거티브 샘플 쌍의 임베딩 간 거리는 멀게 만들도록 학습한다.
- 손실 함수 최적화 — 임베딩 간 유사도(보통 코사인 유사도)를 계산해, 포지티브 쌍의 유사도는 높이고 네거티브 쌍의 유사도는 낮추는 방향으로 손실 함수를 최적화한다.
손실 함수는 모델이 예측한 결과와 실제 정답 간의 오차를 계산해 학습을 조정하는 기준이다. 이번 실습에서는 MultipleNegativesRankingLoss라는 손실 함수를 쓰며, 자세한 설명은 §9.1.4에서 다룬다.
9.1.2 데이터셋 구성
대조 학습에서는 일반적으로 하나의 기준 문서(앵커, anchor)에 대해 하나의 포지티브 샘플과 하나 이상의 네거티브 샘플을 명시적으로 준비해야 한다. 네거티브 샘플은 포지티브 샘플보다 양이 많을수록 좋다. 대조 학습용 데이터셋은 보통 다음 두 방식으로 구성한다.
1. 트리플렛 구성 — 전통적인 방식은 각 학습 데이터를 (앵커, 포지티브, 네거티브) 형태의 트리플렛(triplet)으로 구성하는 것이다. 하나의 앵커에 의미적으로 유사한 문장 하나(포지티브)와 관련 없는 문장 하나(네거티브)를 짝짓는다.
# 전통적인 트리플렛 구성 예
triplets = [
# (앵커, 포지티브, 네거티브)
("강아지를 기르는 방법", "반려견 양육 가이드", "고양이 사료 추천"),
("파이썬 코딩 튜토리얼", "파이썬 프로그래밍 기초", "자바스크립트 입문 강의"),
# ... 수천, 수만 개의 트리플렛 필요
]
2. 다중 네거티브 구성 — 실제 모델 학습에서는 하나의 앵커에 여러 개의 네거티브 샘플을 포함하는 구성이 더 효과적인 경우가 많다.
# 다중 네거티브 샘플 구성 예
training_data = [
{
"anchor": "머신러닝이란?",
"positive": "기계학습은 데이터로부터 패턴을 찾는 AI 기술입니다.",
"negatives": [
"오늘 날씨가 좋네요.",
"내일 회의는 2시에 시작합니다.",
"이 식당의 불고기가 맛있습니다.",
# ... 여러 개의 네거티브 샘플
],
},
# ... 수천 개의 이러한 구조
]
다중 네거티브 구성은 학습 효과를 높일 수 있지만 데이터 준비 난이도도 함께 올라간다. 네거티브 샘플 선정이 특히 까다로운 이유는 다음과 같다.
- 네거티브 샘플 문서는 각 앵커와 반드시 관련 없는 텍스트여야 한다.
- 난이도가 적절해야 한다 — 두 쌍이 너무 관련 없으면 모델이 너무 쉽게 판단해 학습 효과가 거의 없고, 사람이 봐도 관련 여부가 헷갈릴 정도면 난이도가 너무 높아져 오히려 학습을 방해한다.
- 네거티브를 포지티브 대비 몇 배로 구성하느냐에 따라 데이터셋 크기가 기하급수적으로 늘어난다(앵커 수 × 네거티브 수).
이런 이유로 양질의 네거티브 샘플을 구성하는 일은 임베딩 파인튜닝에서 가장 어려운 부분 중 하나다. 이 부담을 덜어주는 방법이 다음 절의 배치 내 네거티브 샘플링이다.
9.1.3 배치 내 네거티브 샘플링
이번 실습은 배치 내에서 네거티브 샘플을 선정하는 학습 방법을 쓴다. 이 방법은 네거티브 샘플을 직접 준비할 필요가 없다는 큰 장점이 있다 — 학습 데이터에서 다른 앵커가 쓰고 있는 샘플(현재 배치 내의 다른 샘플)을 참고해 자동으로 네거티브로 활용한다.
먼저 배치(batch) 개념을 알아야 한다. 모델은 학습할 때 데이터를 1개씩 학습하거나 전체를 한 번에 학습하지 않고, 적당한 개수로 나눈 묶음 단위로 학습한다. 예를 들어 학습 데이터가 5,000개이고 배치 크기가 40이면, 모델은 40개씩 묶어 총 125회(5,000÷40)에 걸쳐 학습한다.
배치 내 네거티브 데이터 생성 방법은 포지티브 샘플만으로 데이터를 구성해도 배치 내에서 네거티브 샘플이 자동으로 만들어지는 학습 방법이다. 사용자는 포지티브 샘플만 제공하면 되고, 네거티브 샘플은 학습 시 배치 내에서 자동 생성된다. 예를 들어 배치 크기가 4라면, 한 번의 학습에 서로 다른 앵커 문서 네 개와 그에 대응하는 포지티브 샘플이 쓰이며, 이들 간 교차로 네거티브 샘플 역할도 동시에 수행한다.
# 배치 크기 4 — 포지티브 쌍만 준비한다
batch = [
("AI란 무엇인가?", "AI는 인간의 지능을 모방한 기술입니다."),
("딥러닝이란?", "신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다."),
("Python은 어디에 쓰이나요?", "Python은 데이터 분석, 웹 개발, AI 등에 널리 사용됩니다."),
("자연어 처리란?", "컴퓨터가 인간의 언어를 이해하고 처리하는 AI의 한 분야입니다."),
]
이때 대조 학습은 다음과 같이 진행된다 — 앵커 "AI란 무엇인가?"의 포지티브는 "AI는 인간의 지능을 모방한 기술입니다."이고, 나머지 세 문서(딥러닝·Python·자연어 처리 답변)는 모두 네거티브로 간주된다. 다른 세 앵커도 같은 방식으로, 자신의 포지티브를 뺀 나머지 문서를 네거티브로 삼는다. 이처럼 하나의 배치 안에서 다른 쌍의 문서를 네거티브로 자동 활용하면서 대조 학습을 수행한다.
9.1.4 MultipleNegativesRankingLoss
손실 함수는 AI 모델이 학습 중에 오차를 계산하고 그 오차를 줄이도록 모델을 업데이트하는 기준이 되는 수식이다. 이번 실습에서는 학습 시 MultipleNegativesRankingLoss라는 손실 함수를 쓴다. 모델은 이 함수의 값을 줄이는 방향으로 학습된다. 수식은 다음과 같다.
L = -log( exp(sim(q, p+)) / (exp(sim(q, p+)) + Σ exp(sim(q, p-))) )
q— 앵커 임베딩(검색어 임베딩)p+— 포지티브 샘플에서 앵커와 연관 있는 문서의 임베딩p-— 네거티브 샘플에서 앵커와 연관 없는 문서의 임베딩sim()— 유사도 함수(일반적으로 코사인 유사도)
이 식은 포지티브 샘플과의 유사도는 높이고, 네거티브 샘플과의 유사도는 낮추도록 설계되어 있다. 학습 과정에서 모델은 이 손실값을 최소화하는 방향으로 학습되며, 학습이 끝나면 포지티브 쌍의 유사도는 높아지고 네거티브 쌍의 유사도는 낮아진 형태로 모델이 업데이트된다.
9.1.5 학습 코드의 이해
다음은 실제 학습에 쓸 구현 코드를 간단히 정리한 예다.
from sentence_transformers import SentenceTransformer, losses, InputExample
from torch.utils.data import DataLoader
# 모델 로드 — 한국어·영어·중국어에 뛰어난 다국어 임베딩 모델
model = SentenceTransformer('BAAI/bge-m3')
# 훈련 데이터 준비 — 의미적으로 유사한 두 문장 쌍으로 구성
train_examples = [
InputExample(texts=["AI란 무엇인가?", "AI는 인간의 지능을 모방한 기술입니다."]),
InputExample(texts=["딥러닝이란?", "신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다."]),
InputExample(texts=["Python은 어디에 쓰이나요?", "Python은 데이터 분석, 웹 개발, AI 등에 널리 사용됩니다."]),
InputExample(texts=["자연어 처리란?", "컴퓨터가 인간의 언어를 이해하고 처리하는 AI의 한 분야입니다."]),
# 실제로는 데이터가 32개보다 훨씬 많다고 가정한다
]
# 배치 크기가 클수록 성능이 향상될 수 있지만 GPU 메모리가 최대 배치 크기를 제한한다
batch_size = 32
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=batch_size)
# MultipleNegativesRankingLoss 설정 — scale은 온도(temperature)의 역수로 손실 강도를 조절한다
train_loss = losses.MultipleNegativesRankingLoss(model, scale=20.0)
# 워밍업 — 전체 학습의 10% 동안 학습률을 점진적으로 증가시킨다
warmup_steps = int(len(train_dataloader) * 0.1)
# 모델 학습
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=3,
warmup_steps=warmup_steps,
optimizer_params={'lr': 2e-5},
output_path='./korean-sentence-embedding-model',
)
먼저 sentence_transformers 라이브러리로 BAAI/bge-m3 모델을 기본 모델로 불러온다. 훈련 데이터는 InputExample 객체 리스트로 준비하는데, 각 예제는 의미적으로 유사한 두 문장 쌍이다. DataLoader는 배치 크기 32로 데이터를 효율적으로 처리하도록 설정한다. 손실 함수는 MultipleNegativesRankingLoss를 채택했으며, scale=20.0은 온도의 역수로서 손실 함수의 강도를 조절한다. 워밍업 단계는 전체 훈련 데이터의 10%로 설정해 학습 초기 안정성을 확보하고, 학습률(lr)은 2e-5로 지정한다. model.fit()을 호출하면 총 3 에포크 동안 모델을 학습시키고, 완성된 모델을 korean-sentence-embedding-model 디렉터리에 저장한다. 이렇게 파인튜닝된 임베딩 모델은 한국어 문장의 의미적 특성을 더 정확히 포착해, 궁극적으로 RAG에서 더 향상된 성능을 낸다.
2. 학습 시 성능을 높이는 방법
9.2.1 배치 크기 키우기
임베딩 모델을 효과적으로 학습시키려면 배치 크기를 크게 설정하는 것이 중요한 전략 중 하나다. 일반적으로 대조 학습은 동일 앵커 기준 네거티브 샘플이 포지티브 샘플보다 많을수록 학습 성능이 올라간다. 이번 실습처럼 배치 내 네거티브 샘플링을 쓰면, 배치 크기가 클수록 더 많은 네거티브 샘플이 자동으로 생성되어 모델 성능이 향상될 가능성이 있다.
| 배치 크기 | 앵커 1개당 네거티브 수 |
|---|---|
| 4 | 3개 |
| 32 | 31개 |
| 128 | 127개 |
다만 배치 크기는 GPU 메모리 용량에 따라 제한되므로 무한정 키울 수 없다. 메모리 한도를 넘으면 학습이 실패하거나 오류가 발생한다. 예를 들어 구글 코랩 무료 GPU에서는 설정 가능한 배치 크기가 3~4 수준에 그치는 경우가 많다.
9.2.2 하드 네거티브 선정
기본적인 배치 내 네거티브 샘플링만으로도 만족할 만한 성능을 낼 수 있지만, 더 어려운 네거티브 샘플, 즉 하드 네거티브(hard negative)를 추가하면 성능을 더 높일 수 있다. 배치 내 네거티브 샘플링이 자동으로 골라내는 네거티브는 이지 네거티브(easy negative)인 반면, 하드 네거티브는 사람이 의도적으로 선별하는 샘플이다.
앞서 본 트리플렛 구성 방식(§9.1.2)을 이용해 하드 네거티브를 다음과 같이 넣을 수 있다.
# 하드 네거티브 예제 — (앵커, 포지티브, 하드 네거티브) 트리플렛 형태
train_examples = [
InputExample(texts=[
"AI란 무엇인가?",
"AI는 인간의 지능을 모방한 기술입니다.",
"AI는 로봇과 같은 물리적 형태를 가진 기계입니다.", # 주제는 같지만 오답인 하드 네거티브
]),
InputExample(texts=[
"딥러닝이란?",
"신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다.",
"컴퓨터가 스스로 생각하는 방법입니다.", # 딥러닝과 혼동하기 쉬운 부정확한 설명
]),
]
각 InputExample의 첫 항목은 앵커(질문), 두 번째는 포지티브 샘플(관련 있는 응답), 세 번째 이후는 하드 네거티브(관련 없지만 구분하기 어려운 응답)다. 손실 함수는 각 (앵커, 포지티브) 쌍에 대해 다른 모든 앵커의 포지티브 샘플과 모든 하드 네거티브 샘플을 네거티브로 사용한다.
일반 네거티브와 하드 네거티브의 예 — 질문 "당뇨병의 증상은 무엇인가요?", 포지티브 "당뇨병의 주요 증상으로는 갈증 증가·빈뇨·체중 감소 등이 있습니다.", 하드 네거티브 "저혈당의 증상으로는 현기증·발한·불안감 등이 있습니다."(의료 관련 주제이지만 당뇨병이 아닌 저혈당 내용), 이지 네거티브 "파이썬은 객체지향 프로그래밍 언어입니다."(완전히 다른 주제). 하드 네거티브는 모델이 더 미묘한 의미 차이를 학습하게 해 정확도를 크게 높일 수 있지만, 구성에 도메인 지식과 추가 작업이 필요하다. 가능하다면 일반 네거티브와 하드 네거티브를 병행하는 것이 이상적이다.
9.2.3 그 외 학습 성능 향상을 위한 팁
- 학습 데이터와 실전의 괴리 최소화 — 앵커는 실제 RAG에서 사용자가 입력할 만한 질문으로 구성해야 한다. 학습 데이터와 실제 질문의 차이가 클수록 학습 후 효용이 떨어진다.
- 데이터 증강 — 난이도 높은 하드 네거티브 샘플을 충분히 확보하면 모델이 더 섬세한 의미 차이를 학습해 성능이 좋아진다.
- 학습률 조정 — 학습률을 바꿔가며 여러 번 학습해 성능을 평가하고 최적의 학습률을 찾는다.
- 온도(temperature) 조정 — 손실 함수의
scale파라미터를 조절하면 학습 강도를 세밀하게 조정할 수 있다.
3. 실전 파인튜닝
이번 실습은 GPU 사용 환경을 전제로 한다. 구글 코랩이라면 상단 메뉴의 [런타임] → [런타임 유형 변경]에서 [T4 GPU]를 선택한다(0장 §2 참고). 실습 코드는 책 저장소의 ch09_EMBEDDING_FINE-TUNING.ipynb 파일이다.
9.3.1 데이터 로드하기
라이브러리 설치
!pip install PyPDF2 datasets sentence-transformers=3.4.1
필요한 도구 임포트
import os
import requests
import json
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
from openai import OpenAI
from torch.utils.data import DataLoader
from sentence_transformers import SentenceTransformer, losses, InputExample
from sentence_transformers.evaluation import InformationRetrievalEvaluator
import torch
from sklearn.metrics.pairwise import cosine_similarity
import PyPDF2
주요 역할만 정리하면 — OpenAI는 GPT 모델로 문서에서 질문을 생성하는 합성 데이터 파이프라인(§9.3.3)에 쓰이고, DataLoader·SentenceTransformer·losses·InputExample은 임베딩 모델 학습(§9.1.5)에, InformationRetrievalEvaluator는 검색 성능 평가(§9.3.5~9.3.8)에, requests·PyPDF2는 실습용 PDF를 내려받아 텍스트로 바꾸는 데(§9.3.2) 쓰인다.
오픈AI 키 설정 — 학습 데이터를 생성하는 데 LLM을 쓴다. API 키를 .env 파일에서 읽어오는 방식은 0장 §5 "파이썬 코드에서 API 키를 안전하게 불러오는 네 가지 방법"과 같다.
load_dotenv("/content/.env")
api_key = os.getenv("OPENAI_API_KEY")
9.3.2 학습·평가용 문서 다운로드
참고 — 원문 표기 차이. 원문은 이 절 제목을 "하드 네거티브 선정"으로 인쇄했다. 그 개념은 이미 §9.2.2에서 다뤘고, 이 절의 실제 내용은 실습용 문서를 내려받아 학습·검증 코퍼스를 만드는 절차다. 학습자료는 내용에 맞춰 제목을 바로잡았다.
책 깃허브 저장소에서 '일본 ICT 동향 문서'와 '미국 ICT 동향 문서' 두 가지를 내려받는다. 여기서는 '미국 ICT 동향 문서'로 임베딩 모델을 학습시키고, 같은 도메인의 '일본 ICT 동향 문서'로 검색 성능을 평가한다. 실제 현업에서도 실제 RAG에 쓸 동일 도메인 데이터로 파인튜닝하면 더 좋은 효과를 얻는다.
# 실습용 문서 다운로드 — 미국 ICT 동향(학습용) · 일본 ICT 동향(검증용)
urls = [
"https://raw.githubusercontent.com/langchain-kr/langchain-tutorial/main/Ch09.%20Embedding%20Fine-tuning/ict_japan_2024.pdf",
"https://raw.githubusercontent.com/langchain-kr/langchain-tutorial/main/Ch09.%20Embedding%20Fine-tuning/ict_usa_2024.pdf",
]
for url in urls:
filename = url.split("/")[-1]
response = requests.get(url)
with open(filename, "wb") as f:
f.write(response.content)
print(f"{filename} 다운로드 완료")
ict_japan_2024.pdf 다운로드 완료
ict_usa_2024.pdf 다운로드 완료
내려받은 두 PDF를 PDF 로더로 읽어 각각 파이썬 문자열 리스트로 만든다.
def extract_text_from_pdf(pdf_path):
"""PDF 파일에서 텍스트를 페이지 단위로 추출하는 함수"""
text_chunks = []
with open(pdf_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
for page_num in range(len(pdf_reader.pages)):
page = pdf_reader.pages[page_num]
text = page.extract_text()
if text.strip():
text = text.strip()
if len(text) > 10: # 의미 있는 내용만 채택 — 너무 짧은 조각은 버린다
text_chunks.append(text)
return text_chunks
train_corpus = extract_text_from_pdf('ict_usa_2024.pdf') # 학습 데이터 — 미국 ICT 동향
print(f'학습 데이터 문서 개수: {len(train_corpus)}')
val_corpus = extract_text_from_pdf('ict_japan_2024.pdf') # 검증 데이터 — 일본 ICT 동향
print(f'검증 데이터 문서 개수: {len(val_corpus)}')
학습 데이터 문서 개수: 26
검증 데이터 문서 개수: 27
extract_text_from_pdf 함수는 PDF를 바이너리 모드로 열어 PyPDF2.PdfReader로 읽고, 페이지 단위로 텍스트를 추출한다. 앞뒤 공백을 제거하고 길이 10자를 넘는 텍스트만 청크로 채택한다. 결과적으로 train_corpus에는 미국 ICT 동향 문서가 청킹되어 26개, val_corpus에는 일본 ICT 동향 문서가 27개 저장된다.
9.3.3 합성 데이터 생성
이번 실습은 배치 내 네거티브 샘플링을 쓰므로 네거티브 샘플은 따로 준비하지 않는다. 하지만 학습에 쓸 포지티브 샘플은 반드시 있어야 한다. GPT 계열 모델 API로 각 문서에 대해 떠오를 만한 질문을 작성하게 해 포지티브 샘플을 자동으로 만든다.
client = OpenAI()
def generate_queries(corpus, num_questions_per_chunk=2):
"""각 문서(청크)마다 질문을 생성해 (질문, 출처 문서) 쌍의 목록을 만든다"""
all_queries = []
all_positive_docs = []
prompt_template = """다음은 참고할 내용입니다.
{context_str}
위 내용을 바탕으로 낼 수 있는 질문을 {num_questions_per_chunk}개 만들어 주세요.
질문만 작성하고 실제 정답이나 보기 등은 작성하지 않습니다.
해당 질문은 본문을 볼 수 없다고 가정합니다.
따라서 '위 본문을 바탕으로~' 라는 식의 질문은 할 수 없습니다.
질문은 아래와 같은 형식으로 번호를 나열하여 생성하십시오.
1. (질문)
2. (질문)"""
for text in tqdm(corpus):
messages = [
{"role": "system", "content": "You are a helpful assistant that generates questions based on provided content."},
{"role": "user", "content": prompt_template.format(
context_str=text,
num_questions_per_chunk=num_questions_per_chunk,
)},
]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7,
)
result = response.choices[0].message.content.strip().split("\n")
questions = []
for line in result:
if line.strip():
parts = line.strip().split('.', 1)
questions.append(parts[1] if len(parts) > 1 else parts[0])
questions = [q for q in questions if len(q) > 0] # 빈 질문 제거
for question in questions:
all_queries.append(question)
all_positive_docs.append(text) # 이 질문이 어느 문서에서 나왔는지 함께 기록
return all_queries, all_positive_docs
generate_queries 함수는 train_corpus·val_corpus처럼 청크로 나뉜 텍스트를 입력받아 각 문서마다 GPT 계열 모델에 질문 생성을 요청한다. num_questions_per_chunk=2는 문서당 질문 2개를 생성하라는 뜻이다. 예를 들어 "2024년 일본의 반도체 산업은 전년 대비 15% 성장했으며, 정부는 300억 엔의 추가 투자를 발표했다."라는 문서가 있다면, "2024년 일본 반도체 산업의 성장률은 얼마인가?"·"일본 정부가 반도체 산업에 발표한 추가 투자금액은?" 같은 질문 두 개를 만들 수 있다. all_queries에는 생성된 모든 질문이, all_positive_docs에는 각 질문의 출처 문서가 같은 인덱스로 매칭되어 저장된다.
train_queries, train_positive_docs = generate_queries(train_corpus)
print(f'생성된 학습용 질문 개수: {len(train_queries)}')
val_queries, val_positive_docs = generate_queries(val_corpus)
print(f'생성된 검증용 질문 개수: {len(val_queries)}')
생성된 학습용 질문 개수: 52
생성된 검증용 질문 개수: 54
문서 26개에서 문서당 질문 2개씩 만들어 학습용 질문 52개, 문서 27개에서 검증용 질문 54개가 나왔다. 이제 이 데이터를 InputExample 객체 리스트로 변환한다.
train_examples = []
for query, doc in zip(train_queries, train_positive_docs):
example = InputExample(texts=[query, doc])
train_examples.append(example)
참고로 실무에 적용할 때는 책이 쓴 프롬프트를 그대로 쓰기보다, §9.2.3에서 말한 대로 실제 RAG 시스템에서 사용자가 입력할 만한 질문이 나오도록 프롬프트를 조정하는 것이 바람직하다.
9.3.4 모델 로드하기
배치 크기, 학습할 모델, 손실 함수를 설정한다. 여기서는 코랩 GPU의 한계로 매우 작은 배치 크기 4를 쓴다 — 배치가 클수록 더 많은 네거티브 샘플이 생겨 성능이 좋아질 수 있다는 것은 §9.2.1에서 다뤘다.
BATCH_SIZE = 4
loader = DataLoader(train_examples, batch_size=BATCH_SIZE, shuffle=True)
model_id = "BAAI/bge-m3" # 한국어에서 상대적으로 뛰어난 성능을 보이는 다국어 모델
model = SentenceTransformer(model_id)
loss = losses.MultipleNegativesRankingLoss(model)
9.3.5 평가 데이터 전처리
검색 성능 평가에는 InformationRetrievalEvaluator를 쓴다. 이 도구는 다음 세 가지 데이터 구조를 입력받는다.
- queries — 질문 ID를 키, 질문 텍스트를 값으로 갖는 딕셔너리.
- corpus — 문서 ID를 키, 문서 텍스트를 값으로 갖는 딕셔너리.
- relevant_docs — 질문 ID를 키, 그 질문과 관련된 문서 ID들의 집합(set)을 값으로 갖는 딕셔너리.
val_dataset = {
'queries': {},
'corpus': {},
'relevant_docs': {},
}
# 문서 ID를 먼저 생성
doc_ids = {}
for i, doc in enumerate(val_corpus):
doc_id = f"d{i}"
val_dataset['corpus'][doc_id] = doc
doc_ids[doc] = doc_id
# 질문에 ID를 부여하고 관련 문서를 연결
for i, (query, doc) in enumerate(zip(val_queries, val_positive_docs)):
query_id = f"q{i}"
val_dataset['queries'][query_id] = query
doc_id = doc_ids[doc]
val_dataset['relevant_docs'].setdefault(query_id, set()).add(doc_id)
dataset = val_dataset
dataset['corpus']·dataset['queries']·dataset['relevant_docs']를 각각 corpus·queries·relevant_docs에 저장해 평가 도구에 전달한다.
corpus = dataset['corpus'] # 검색 대상 문서
queries = dataset['queries'] # 검색어(쿼리)
relevant_docs = dataset['relevant_docs'] # 각 쿼리와 관련된 문서(포지티브)
evaluator = InformationRetrievalEvaluator(queries, corpus, relevant_docs)
9.3.6 모델 학습하기
데이터 양이 적으므로 학습 횟수(에포크)는 2로 충분하다.
EPOCHS = 2
os.environ["WANDB_DISABLED"] = "true" # 학습 추적·시각화 도구(W&B) 비활성화
warmup_steps = int(len(loader) * EPOCHS * 0.1) # 전체 학습의 10%를 워밍업으로
model.fit(
train_objectives=[(loader, loss)],
epochs=EPOCHS,
warmup_steps=warmup_steps,
output_path='exp_finetune',
show_progress_bar=True,
)
train_objectives=[(loader, loss)]로 데이터 로더와 손실 함수를 연결하고, output_path='exp_finetune'에 파인튜닝된 모델을 저장한다. GPU를 쓰면 데이터가 소량이라 수 분 안에 학습이 끝난다.
9.3.7 검색 성능 평가 지표
InformationRetrievalEvaluator가 계산하는 여섯 가지 대표 지표를 정리한다.
- Accuracy@k — 정답이 상위 k개 검색 결과 안에 하나라도 포함되면 성공으로 센다. 위치·개수는 고려하지 않는다. 예: [정답,오답,오답,오답,오답]·[오답,오답,정답,오답,오답]·[오답,오답,오답,오답,오답] 세 질문 중 두 질문이 성공이면 Accuracy@5 = 2/3 ≈ 0.667.
- Precision@k — 상위 k개 결과 중 정답이 차지하는 비율. 예: 질문 세 개의 Precision@5가 각각 0.2·0.2·0.0이면 평균 0.133.
- Recall@k — 전체 정답 중 상위 k개 안에 포함된 정답의 비율. 이번 실습처럼 질문당 정답이 하나뿐이면 Recall과 Accuracy 값이 같다.
- NDCG — 정답이 높은 순위에 있을수록 높은 점수를 준다. 포함 여부만이 아니라 순위의 유용성까지 본다. 예: [정답,정답,오답]→1.0, [오답,정답,오답]→약 0.63, [오답,오답,정답]→약 0.39.
- MRR — 정답이 처음 등장한 순위의 역수를 평균한다. 예: 1위면 1.0, 2위면 0.5, 3위면 0.33, 10위 밖이면 0.
- MAP — 정답을 찾을 때마다의 Precision을 평균한다. 예: [정답,정답,오답,오답,정답]이면 Precision@1=1.0·Precision@2=1.0·Precision@5=0.6이고 이 셋의 평균이 MAP다.
Accuracy·Precision·Recall은 정답의 포함 여부만 보고, NDCG·MRR·MAP은 정답이 어느 순위에 있는지까지 본다 — 이 구분은 부록 A에서 표로 다시 정리한다.
9.3.8 파인튜닝 모델 평가하기
InformationRetrievalEvaluator는 다음 순서로 동작한다.
- 모델이
queries의 각 질문과corpus의 모든 문서를 임베딩 벡터로 변환한다. - 각 질문 벡터와 모든 문서 벡터 간 코사인 유사도를 계산한다.
- 각 질문마다 문서를 유사도 순으로 정렬한다.
relevant_docs에 명시된 정답 문서가 그 정렬 리스트에서 몇 위인지 확인한다.- MRR·NDCG·Precision@k·Recall@k 등을 계산한다.
이를 감싸는 평가 함수를 만든다.
def evaluate_st(dataset, model_id, name, evaluator):
"""SentenceTransformer 모델의 검색 성능을 평가하고 CSV로 저장하는 함수"""
os.makedirs('results', exist_ok=True)
model = SentenceTransformer(model_id)
result = evaluator(model)
result_df = pd.DataFrame([result]) if isinstance(result, dict) else result
output_path = f'results/Information-Retrieval_evaluation_{name}_results.csv'
result_df.to_csv(output_path, index=False)
return result
# 원본 모델 평가
evaluate_st(dataset=val_dataset, model_id="BAAI/bge-m3", name='original', evaluator=evaluator)
# 파인튜닝된 모델 평가
evaluate_st(dataset=val_dataset, model_id="exp_finetune", name='finetuned', evaluator=evaluator)
df_st_original = pd.read_csv('results/Information-Retrieval_evaluation_original_results.csv')
df_st_finetuned = pd.read_csv('results/Information-Retrieval_evaluation_finetuned_results.csv')
df_st_original['model'] = 'bge-m3'
df_st_finetuned['model'] = 'fine_tuned'
df_st_all = pd.concat([df_st_original, df_st_finetuned]).set_index('model')
print(df_st_all)
| model | cosine-Accuracy@5 | cosine-Accuracy@10 | cosine-Precision@1 | cosine-Recall@3 |
|---|---|---|---|---|
| bge-m3 | 0.944444 | 0.981481 | 0.814815 | 0.944444 |
| fine_tuned | 0.981481 | 1.000000 | 0.870370 | 0.981481 |
파인튜닝 전 모델이 이미 1.0으로 최고 점수를 받은 지표에서는 동점이었지만, 나머지 모든 지표에서 파인튜닝 후 모델이 더 높은 성능을 보였다. 특정 도메인에 맞게 임베딩 모델을 파인튜닝하면 기존 모델보다 더 우수한 검색 성능을 얻을 수 있다는 뜻이다. 다만 실제 환경에 적용할 때는 이번 실습보다 다양한 요소(더 많은 학습 데이터, 하드 네거티브, 학습률 튜닝 등 — §9.2 참고)를 고려해야 한다.
핵심 개념 정리
| 개념 | 한 줄 설명 |
|---|---|
| 대조 학습 | 포지티브 쌍은 가깝게, 네거티브 쌍은 멀게 임베딩을 조정하는 학습 방식 |
| 포지티브·네거티브 샘플 | 앵커와 관련 있는 문서(포지티브) vs 관련 없는 문서(네거티브) — 대조 학습의 재료 |
| 트리플렛 vs 다중 네거티브 | (앵커,포지티브,네거티브) 한 쌍씩 구성 vs 앵커 하나에 네거티브 여러 개 — 후자가 효과적이나 준비가 더 어렵다 |
| 배치 내 네거티브 샘플링 | 포지티브만 준비해도 같은 배치의 다른 샘플이 자동으로 네거티브가 되는 방법 |
| MultipleNegativesRankingLoss | 포지티브 유사도는 높이고 네거티브 유사도는 낮추도록 설계된 손실 함수 |
| 배치 크기와 네거티브 수 | 배치 크기가 클수록 앵커 1개당 자동 생성되는 네거티브 수가 늘어난다(단, GPU 메모리가 상한) |
| 이지 네거티브 vs 하드 네거티브 | 배치 내 자동 선택(쉬움) vs 사람이 고른 헷갈리는 오답(어려움, 성능 향상 효과 큼) |
| 합성 데이터 생성 | LLM에게 문서마다 질문을 만들게 해 포지티브 샘플(앵커-문서 쌍)을 자동 확보하는 방법 |
| InformationRetrievalEvaluator | queries·corpus·relevant_docs 세 딕셔너리로 임베딩 모델의 검색 성능을 평가하는 도구 |
| 평가 지표 6종 | Accuracy·Precision·Recall(포함 여부) / NDCG·MRR·MAP(순위까지 고려) |
실무 체크리스트
- [ ] 학습 데이터의 앵커가 실제 RAG에서 사용자가 입력할 법한 질문 형태인가?
- [ ] 포지티브 샘플만이 아니라 하드 네거티브를 추가할 여지가 있는지 검토했는가?
- [ ] 배치 크기를 GPU 메모리가 허용하는 한도까지 키웠는가?
- [ ] 네거티브 샘플의 난이도가 "너무 쉬움"과 "너무 헷갈림" 사이에 있는가?
- [ ] 합성 데이터 생성에 쓰는 LLM 모델이 지금도 지원되는 모델인가(§최신 동향 참고)?
- [ ] 학습·평가 코퍼스가 실제로 적용할 도메인과 같은 종류의 문서인가?
- [ ] 평가할 때 Accuracy만 보지 않고 순위를 고려하는 NDCG·MRR도 함께 확인했는가?
- [ ] 워밍업 스텝·학습률·온도(scale)를 데이터 규모에 맞게 조정했는가?
- [ ] 원본 모델과 파인튜닝 모델을 같은 평가 데이터로 비교했는가(공정한 비교인가)?
연습문제
- 적용. 사내 계약서 검색 시스템을 만든다고 하자. 일반 임베딩 모델 대신 이 장의 방법으로 파인튜닝하는 것이 왜 도움이 되는지, 그리고 학습 데이터의 앵커를 무엇으로 구성해야 하는지 설명하라.
- 판단. 학습 데이터가 5,000개이고 GPU 메모리가 배치 크기 16까지만 허용한다고 하자. 배치 내 네거티브 샘플링만 쓸 때와 하드 네거티브를 추가로 준비할 때, 각각 기대할 수 있는 효과와 필요한 추가 작업을 비교하라.
- 비교. 검색 결과 상위 5개 중 정답이 3번째 위치에 딱 하나 있는 경우를 가정해, Accuracy@5·Recall@5·NDCG@5 세 지표의 값이 왜 서로 다르게 나오는지 설명하라.
- 설계. 합성 데이터 생성 파이프라인(§9.3.3)을 실제 사내 문서에 적용하려 한다. 책이 쓴 프롬프트를 그대로 쓰면 안 되는 이유와, 프롬프트를 어떻게 바꿔야 하는지 §9.2.3의 원칙에 근거해 제안하라.
최신 동향 (2026-09 기준)
최신 동향 (검증 2026-09-12) — 이 장의 원리(대조 학습·배치 내 네거티브 샘플링·MultipleNegativesRankingLoss)는 그대로 유효하다. 다만 구체적인 도구 선택 세 가지는 책 집필 이후 바뀌었다.
- 학습 API — 이 장의
model.fit()방식은 지금도 동작하지만(하위 호환), sentence-transformers 공식 문서는 v3 이후 Hugging FaceTrainer스타일의SentenceTransformerTrainer를 표준 학습 경로로 안내한다.MultipleNegativesRankingLoss자체는 지금도 포지티브 쌍만 있을 때 쓰는 기본 손실 함수로 유효하며, 배치 내에서 우연히 포지티브인데 네거티브로 취급되는 "가짜 네거티브" 문제를 줄인GISTEmbedLoss가 파생 손실로 추가됐다. (Sentence Transformers Losses) - 임베딩 모델 선택 — 이 장이 쓰는
BAAI/bge-m3는 다국어·하이브리드(밀집+희소+멀티벡터) 검색을 한 모델로 지원해 지금도 널리 쓰이는 오픈소스 기본값이지만, 벤치마크 최상위권은 이후 나온 모델들로 옮겨갔다. 순위 자체보다 "지금도 실무 기본값으로 쓰이는가"가 중요하며 bge-m3는 그 기준을 만족한다. (MTEB 리더보드) - 합성 데이터 생성 모델 — 이 장이 질문 생성에 쓰는
gpt-4o는 이후 ChatGPT 목록에서 빠졌고 API 스냅샷들도 단계적으로 폐기 일정에 올랐다. 같은 파이프라인을 새로 구성한다면 그 시점에 지원되는 최신 GPT 계열 모델로 바꿔야 한다. (OpenAI API Deprecations)
부록 A. 핵심 비교표
이지 네거티브 vs 하드 네거티브
| 구분 | 이지 네거티브 | 하드 네거티브 |
|---|---|---|
| 선택 방식 | 배치 내에서 자동 생성 | 사람이 직접, 의도적으로 선택 |
| 포지티브와의 관계 | 대부분 주제가 완전히 다름 | 주제는 유사하나 정확한 답변이 아님 |
| 모델 학습 난이도 | 상대적으로 쉬움 | 미묘한 의미 차이를 요구해 더 어려움 |
| 준비 비용 | 없음(자동) | 도메인 지식과 추가 작업 필요 |
데이터 준비 방식 비교
| 구분 | 트리플렛/다중 네거티브(명시적 준비) | 배치 내 네거티브 샘플링 |
|---|---|---|
| 필요한 재료 | 앵커+포지티브+네거티브 전부 사람이 준비 | 앵커+포지티브 쌍만 준비 |
| 데이터셋 크기 증가 | 네거티브 배수만큼 기하급수적으로 증가 | 배치 크기에 비례(추가 준비 없음) |
| 네거티브 품질 | 사람이 통제 가능(하드 네거티브 포함 가능) | 배치 구성에 좌우(이지 네거티브 위주) |
검색 평가 지표 — 포함 여부만 보는가, 순위까지 보는가
| 구분 | Accuracy·Precision·Recall | NDCG·MRR·MAP |
|---|---|---|
| 보는 것 | 정답이 상위 k개 안에 있는지(포함 여부) | 정답이 몇 위에 있는지(순위) |
| 정답이 1위든 5위든 | 같은 값(포함되면 그만) | 값이 달라짐(앞쪽일수록 높은 점수) |
| 질문당 정답이 하나뿐일 때 | Recall = Accuracy | MRR·NDCG는 여전히 순위를 반영 |
부록 B. 추천 참고 자료
외부 자료 (Tier 1 공식, 생존 확인 2026-09-12)
- 책 공식 실습 노트북 (GitHub) — 이 장 전체 코드의 원본. ch09_EMBEDDING_FINE-TUNING.ipynb
- sentence-transformers 공식 Losses 문서 —
MultipleNegativesRankingLoss의 정확한 정의와GISTEmbedLoss등 관련 손실 함수. Losses — Sentence Transformers - sentence-transformers 공식 학습 가이드 —
model.fit()이후 표준이 된SentenceTransformerTrainer학습 흐름. Training Overview — Sentence Transformers - BAAI/bge-m3 공식 모델 카드 — 이 장이 쓰는 모델의 원본 문서(다국어·하이브리드 검색 지원 범위). BAAI/bge-m3
- MTEB 리더보드 — 임베딩 모델 검색 성능을 비교하는 공식 벤치마크. MTEB Leaderboard
- OpenAI API Deprecations —
gpt-4o계열 모델의 폐기 일정과 대체 모델 공식 안내. OpenAI API Deprecations
본 책 연계 챕터
| 챕터 | 이 장이 다루지 않은 것 |
|---|---|
| 2장 §4 벡터 데이터베이스 | 파인튜닝한 임베딩 모델을 실제로 저장하고 검색에 쓰는 인프라(크로마·파이스) |
| 2장 §1 검색 증강 생성 개요 | 이 장의 모든 실습이 전제로 삼는 임베딩 기반 검색의 출발점과 전체 RAG 구조 |
| 4장 §3 검색 알고리즘 | 이 장에서 파인튜닝한 밀집 검색 모델을 희소·앙상블 검색과 결합해 실전에 쓰는 방법 |
| 8장 §4 로컬 LLM Qwen 파인튜닝하기 | 같은 "파인튜닝" 작업을 LLM 본체에 적용한 사례 — 이 장은 임베딩 모델, 8장은 LLM이 대상이라는 차이를 대조해 보면 좋다 |
부록 C. 연습문제 풀이
-
(계약서 검색 시스템 적용) 일반 임베딩 모델은 계약서에 자주 나오는 법률 용어·조항 표현의 미묘한 의미 차이를 충분히 구분하지 못할 수 있다. 이 장의 방법으로 파인튜닝하면 대조 학습을 통해 그 도메인 특유의 유사·비유사 관계를 모델에 학습시켜 검색 정확도를 높일 수 있다. 앵커는 실제 사용자가 입력할 법한 질문 형태(예: "이 계약의 해지 조건은?")로 구성해야 하며, 포지티브 샘플은 그 질문에 실제로 답이 되는 계약서 조항이어야 한다.
-
(배치 내 네거티브 vs 하드 네거티브 비교) 배치 크기 16까지만 허용되면 배치 내 네거티브 샘플링만으로는 앵커 1개당 최대 15개의(대부분 이지 네거티브) 네거티브만 확보된다 — 추가 준비 없이 바로 쓸 수 있지만 미묘한 의미 차이는 학습하기 어렵다. 하드 네거티브를 추가하면 모델이 더 어려운 구분을 학습해 정확도가 오르지만, 도메인 지식을 가진 사람이 "관련은 있지만 정답은 아닌" 문서를 직접 골라야 하므로 데이터 준비 비용이 커진다. 두 방법은 배타적이지 않으며, 배치 내 네거티브로 기본 성능을 확보한 뒤 하드 네거티브로 보강하는 병행이 이상적이다.
-
(세 지표가 다르게 나오는 이유) 정답이 상위 5개 중 3번째에 있으므로 Accuracy@5는 정답 포함 여부만 보아 1.0(성공)이다. Recall@5도 이 질문의 정답이 하나뿐이므로 역시 1.0이다. 반면 NDCG@5는 정답의 위치(순위)에 따라 점수를 할인하므로, 1위에 있을 때보다 3위에 있을 때 더 낮은 값이 나온다 — 포함 여부만 보는 두 지표와 달리, 순위가 늦을수록 사용자가 정답을 찾는 데 더 많은 노력이 들었다는 사실을 점수에 반영하기 때문이다.
-
(합성 데이터 프롬프트 재설계) 책의 프롬프트는 GPT 계열 모델이 문서 내용만 보고 일반적인 질문을 만들도록 지시한다. 그대로 쓰면 실제 사내 문서 검색에서 사용자가 입력할 질문과 괴리가 생길 수 있다(§9.2.3의 "학습 데이터와 실전의 괴리 최소화" 원칙). 따라서 프롬프트에 실제 서비스의 사용자 질의 스타일(문장 길이, 구어체 여부, 도메인 용어 사용 빈도)에 대한 예시나 지침을 추가하고, 생성된 질문 일부를 사람이 검수해 실제 질의와 얼마나 가까운지 확인한 뒤 반복 조정하는 것이 바람직하다.
클릭하거나 Space를 눌러 뒤집기